/**
 * 
 */
package ro.dta.idbi.model.dynamicpattern;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.postgresql.jdbc3.Jdbc3ResultSet;
import org.postgresql.util.Base64;

import ro.dta.idbi.interfaces.IGraphHash;
import ro.dta.idbi.model.AbstractPattern;
import ro.dta.idbi.model.TransformationException;
import uk.ac.ic.doc.automed.DSR;
import uk.ac.ic.doc.automed.DSRException;
import uk.ac.ic.doc.automed.IntegrityException;
import uk.ac.ic.doc.automed.NotFoundException;
import uk.ac.ic.doc.automed.reps.Schema;

import com.mxgraph.model.mxIGraphModel;

/**
 * Class used to represent a BAV transaction
 * 
 * @author Tudor Dobrila
 * 
 */
public class Transaction {

	/**
	 * Transaction id
	 */
	private int tid;

	/**
	 * Hash code of the signature of the transaction
	 */
	private String hashCode;

	/**
	 * Sequence of instructions belonging to the
	 */
	private String[] instructions;

	/**
	 * Signature of the transaction
	 */
	private DependencyGraph signature;

	/**
	 * Name of the original schema, before the application of the transaction
	 */
	private String originalSchema;

	/**
	 * Name of the final schema, after the application of the transaction
	 */
	private String finalSchema;

	/**
	 * Hash algorithm used to compute the hash of the signature
	 */
	private IGraphHash hashAlgorithm;

	/**
	 * Transaction class constructor
	 * 
	 * @param hashAlgorithm
	 *            Hash algorithm used to compute the hash of the signature
	 * @param instructions
	 *            Sequence of instructions in the transaction
	 * @param originalSchema
	 *            Name of the original schema, before the application of the
	 *            transaction
	 * @throws ParseException
	 *             If one of the instructions is invalid
	 */
	public Transaction(IGraphHash hashAlgorithm, String[] instructions, String originalSchema)
			throws ParseException {
		this.instructions = instructions;
		this.originalSchema = originalSchema;
		this.hashAlgorithm = hashAlgorithm;
	}

	/**
	 * Transaction class constructor
	 * 
	 * @param instructions
	 *            Sequence of instructions in the transaction
	 * @param signature
	 *            Signature of the transaction
	 * @param hashCode
	 *            Hash of the signature of the transaction
	 * @param originalSchema
	 *            Name of the original schema, before the application of the
	 *            transaction
	 * @param finalSchema
	 *            Name of the final schema, after the application of the
	 *            transaction
	 */
	public Transaction(String[] instructions, DependencyGraph signature, String hashCode,
			String originalSchema, String finalSchema) {
		this.instructions = instructions;
		this.signature = signature;
		this.hashCode = hashCode;
		this.originalSchema = originalSchema;
		this.finalSchema = finalSchema;
	}

	/**
	 * Transaction class constructor
	 * 
	 * @param tid
	 *            Transaction ID
	 * @param instructions
	 *            Sequence of instructions in the transaction
	 * @param signature
	 *            Signature of the transaction
	 * @param hashCode
	 *            Hash of the signature of the transaction
	 * @param originalSchema
	 *            Name of the original schema, before the application of the
	 *            transaction
	 * @param finalSchema
	 *            Name of the final schema, after the application of the
	 *            transaction
	 */
	public Transaction(int tid, String[] instructions, DependencyGraph signature, String hashCode,
			String originalSchema, String finalSchema) {
		this(instructions, signature, hashCode, originalSchema, finalSchema);
		this.tid = tid;
	}

	/**
	 * Save the transaction to the database
	 */
	public void save() throws IOException, DSRException, SQLException {
		StringBuilder builder = new StringBuilder();
		for (String instr : instructions) {
			builder.append(instr).append("\n");
		}

		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		ObjectOutputStream oos = new ObjectOutputStream(baos);
		oos.writeObject(signature.getModel());
		String sign = Base64.encodeBytes(baos.toByteArray());
		baos.close();
		oos.close();

		String query = "";
		if (tid == 0) {
			Jdbc3ResultSet result = (Jdbc3ResultSet) DSR.executeStatement("STR",
					"SELECT COALESCE(MAX(tid),0) FROM transaction", false);
			if (result.next()) {
				tid = result.getInt(1) + 1;
			} else {
				tid = 1;
			}
			query = "INSERT INTO transaction VALUES (" + tid + ", '" + builder.toString() + "', '"
					+ hashCode + "', '" + sign + "', '" + originalSchema + "', '" + finalSchema
					+ "')";
		} else {
			query = "UPDATE transaction SET instructions = '" + builder.toString()
					+ "', hashcode = '" + hashCode + "', signature = '" + sign + "' WHERE tid = "
					+ tid;
		}

		DSR.executeStatement("STR", query, false);
	}

	/**
	 * Load the transaction with the TID from the AutoMed repository
	 * 
	 * @param tid
	 *            Transaction ID
	 * @return Transaction with the TID passed as parameter, null if no such
	 *         transaction exists
	 */
	public static Transaction loadTransaction(int tid) throws DSRException, IOException,
			SQLException, ClassNotFoundException {
		String query = "SELECT * FROM transaction WHERE tid = " + tid;
		Jdbc3ResultSet transQuery = (Jdbc3ResultSet) DSR.executeStatement("STR", query, false);

		if (transQuery.next()) {
			String instrStr = transQuery.getString(2);
			String[] instructions = instrStr.split("\n");
			String crtHash = transQuery.getString(3);
			String signatureStr = transQuery.getString(4);
			String originalSchema = transQuery.getString(5);
			String finalSchema = transQuery.getString(6);
			byte[] signBytes = Base64.decode(signatureStr);

			ByteArrayInputStream bais = new ByteArrayInputStream(signBytes);
			ObjectInputStream ois = new ObjectInputStream(bais);
			mxIGraphModel model = (mxIGraphModel) ois.readObject();
			DependencyGraph signature = new DependencyGraph(model);
			Transaction crtTrans = new Transaction(tid, instructions, signature, crtHash,
					originalSchema, finalSchema);
			return crtTrans;
		}

		return null;
	}

	/**
	 * Get all transactions in the database that have the hash equal to the
	 * value
	 * 
	 * @param hashCode
	 *            Hash code to check for
	 * @return List of transactions with the hash equal to the one passed as
	 *         parameter
	 */
	public static List<Transaction> loadTransactions(String hashCode) throws DSRException,
			IOException, SQLException, ClassNotFoundException {
		List<Transaction> result = new ArrayList<Transaction>();
		String query = "SELECT tid FROM transaction";
		if (hashCode != null) {
			query += " WHERE hashcode = '" + hashCode + "'";
		}
		Jdbc3ResultSet transQuery = (Jdbc3ResultSet) DSR.executeStatement("STR", query, false);

		while (transQuery.next()) {
			int id = transQuery.getInt(1);
			result.add(Transaction.loadTransaction(id));
		}

		return result;
	}

	/**
	 * Execute the transaction, only if it has not been previously executed,
	 * i.e. ID is equal to 0
	 * 
	 * @throws TransactionException
	 *             If an error occured in the middle of the transaction
	 */
	public void execute() throws TransactionException {
		if (tid == 0 && instructions.length > 0) {
			Schema schema;
			try {
				schema = Schema.getSchema(originalSchema);
				Schema firstSchema = AbstractPattern.execute(schema, instructions[0]);
				Schema crtSchema = firstSchema;

				for (int i = 1; i < instructions.length; i++) {
					String instr = instructions[i];
					try {
						crtSchema = AbstractPattern.execute(crtSchema, instr);
					} catch (TransformationException e) {
						try {
							firstSchema.oldRetract();
						} catch (IntegrityException e1) {
							throw new TransactionException(
									"Could not retract to the original schema. Transaction produced integrity issues at line:\r\n"
											+ instr, e);
						}
						throw new TransactionException(
								"Could not execute the following instruction:\r\n" + instr, e);
					}
				}
				this.signature = new DependencyGraph(instructions);
				this.signature.collapse();
				this.hashCode = hashAlgorithm.computeHash(signature.getAdjacencyMatrix());
				this.finalSchema = crtSchema.getName();
			} catch (NotFoundException e1) {
				throw new TransactionException("Original schema does not exist!", e1);
			} catch (TransformationException e) {
				throw new TransactionException("Could not execute the following instruction:\r\n"
						+ instructions[0], e);
			} catch (ParseException e) {
				throw new TransactionException(
						"Could not compute the signature of the transaction!Reason:\r\n"
								+ e.getMessage(), e);
			}
		}
	}

	/**
	 * @return Hash code of the transaction
	 */
	public String getHashCode() {
		return hashCode;
	}

	/**
	 * @param instructions
	 *            Set the sequence of instructions in the transaction
	 */
	public void setInstructions(String[] instructions) {
		this.instructions = instructions;
	}

	/**
	 * @return Sequence of instructions in the transaction
	 */
	public String[] getInstructions() {
		return instructions;
	}

	/**
	 * @param signature
	 *            The new signature of the transaction
	 */
	public void setSignature(DependencyGraph signature) {
		this.signature = signature;
	}

	/**
	 * @return Signature of the transaction
	 */
	public DependencyGraph getSignature() {
		return signature;
	}

	/**
	 * @param tid
	 *            The new transaction ID to set
	 */
	public void setTid(int tid) {
		this.tid = tid;
	}

	/**
	 * @return ID of the transaction
	 */
	public int getTid() {
		return tid;
	}

	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		builder.append("ID: ").append(tid).append("\n");
		if (instructions != null && instructions.length != 0) {
			builder.append("Instructions: \n");
			for (String instr : instructions) {
				builder.append(instr).append("\n");
			}
		}
		builder.append("Hash code: ").append(hashCode).append("\n");
		return builder.toString();
	}

	/**
	 * @return Name of the original schema, before the application of the
	 *         transaction
	 */
	public String getOriginalSchema() {
		return originalSchema;
	}

	/**
	 * @param originalSchema
	 *            New name of the original schema, before the application of the
	 *            transaction
	 */
	public void setOriginalSchema(String originalSchema) {
		this.originalSchema = originalSchema;
	}

	/**
	 * @return Name of the final schema, after the application of the
	 *         transaction
	 */
	public String getFinalSchema() {
		return finalSchema;
	}

	/**
	 * @param finalSchema
	 *            New name of the final schema, after the application of the
	 *            transaction
	 */
	public void setFinalSchema(String finalSchema) {
		this.finalSchema = finalSchema;
	}

	/**
	 * Randomly generate a transaction
	 * 
	 * @return Randomly generated transaction
	 */
	public static Transaction generateTransaction(IGraphHash hashAlgorithm) {
		String[] operation = new String[] { "add", "delete", "rename", "extend", "contract" };
		String[] construct = new String[] { "table", "column", "primarykey", "foreignkey" };

		SecureRandom random = new SecureRandom();
		List<String> objects = new ArrayList<String>();
		List<String> tables = new ArrayList<String>();
		int noTables = random.nextInt(3) + 3;
		int noColumns = random.nextInt(3) + 3;

		for (int i = 0; i < noTables; i++) {
			String name = new BigInteger(40, random).toString(32);
			objects.add("<<" + name + ">>");
			tables.add(name);
		}

		for (int i = 0; i < noColumns; i++) {
			String name = new BigInteger(40, random).toString(32);
			int tableIndex = random.nextInt(noTables);
			objects.add("<<" + tables.get(tableIndex) + "," + name + ">>");
		}

		int numTrans = random.nextInt(17) + 3;
		AtomicNode[] nodes = new AtomicNode[numTrans];
		for (int i = 0; i < numTrans; i++) {
			String crtOp = operation[random.nextInt(operation.length)];
			String crtConstr = construct[random.nextInt(construct.length)];
			int crtObjIndex = random.nextInt(objects.size());
			String crtObj = objects.get(crtObjIndex);

			int numDependencies = random.nextInt(4) + 1;
			int j = 0;
			Set<String> depObjs = new HashSet<String>();
			while (j < numDependencies) {
				int crtDepIndex = random.nextInt(objects.size());
				String crtDep = objects.get(crtDepIndex);
				if (depObjs.contains(crtDep) || crtDep.equals(crtObj)) {
					continue;
				}

				depObjs.add(crtDep);
				j++;
			}

			nodes[i] = new AtomicNode(crtOp + "_" + crtConstr, crtObj, depObjs);
		}

		DependencyGraph graph;
		try {
			graph = new DependencyGraph(nodes);
			graph.collapse();
			return new Transaction(new String[] {}, graph, hashAlgorithm.computeHash(graph
					.getAdjacencyMatrix()), "", "");
		} catch (ParseException e) {
			e.printStackTrace();
		}

		return null;
	}

}
